using Pegasus.Text;
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

using Pegasus.Log4Net;

namespace Pegasus.Diagnostics.Telnet
{
	/// <summary>
	/// The TelnetProcessor class implements a general command line processor that can be
	/// attached to any application or service.  This class uses an TcpClient stream to take input 
	/// and parse the command and then calls the ITelnetCommand interface to process the command
	/// and return any text using the output stream of the TcpClient object.
	/// </summary>
	/// <remarks>
	/// This class is marked as internal and should not be used out side the pegasus library.
	/// </remarks>
	internal class TelnetProcessor
	{
		// Local Instance Values
		private ILog m_log = LogManager.GetLogger( typeof( TelnetProcessor ) );

		private TcpClient m_tcpClient;
		private StreamReader m_inStream;
		private StreamWriter m_outStream;

		private Thread m_workingThread;
		private bool m_done = false;
		private CommandListen m_commandListen = new CommandListen();

		/// <summary>Holds the commands history for this session</summary>
		private ArrayList m_historyCommands = new ArrayList();

		/// <summary>Table command interface object, accessed by thier name</summary>
		private SortedList m_commandsList = new SortedList();

		/// <summary>Table command interface object, accessed by thier name or short cut</summary>
		private SortedList m_allCommandsList = new SortedList();

		/// <summary>
		/// Called to initialize a TelnetProcessor object from a socket.  This constructor
		/// will initialize the object using the input and output streams from the socket.
		/// The socket will be passed to a NetworkStream and then a StreamReader (which
		/// implements TextReader) and StreamWriter (which implements TextWriter) will be
		/// created from the NetworkStream.
		/// </summary>
		/// <param name="tcpClient">
		/// Reference to the TcpClient socket that the object will be initialized from.
		/// </param>
		/// <param name="customCommands">ArrayList of custom command objects</param>
		public TelnetProcessor( TcpClient tcpClient, ArrayList customCommands )
		{
			// Check params
			ParamCode.AssertNotNull( tcpClient, "tcpClient" );
			// customCommands can be null.

			m_tcpClient = tcpClient;

			// Call the private method that constructs the object
			NetworkStream netStream = tcpClient.GetStream();

			m_inStream = new StreamReader( netStream, new TelnetEncoding() );
			m_outStream = new StreamWriter( netStream, new TelnetEncoding() );

			// Set auto flush to true for the output stream.  This will force a flush
			// of the buffer after every Stream.Write call.
			m_outStream.AutoFlush = true;

			// Add the system commands
			AddTelnetCommands( new ITelnetCommand[] { 
				new CommandQuit( this ),
				new CommandHelp( this ),
				m_commandListen,
				new CommandTrace(),
				new CommandVersion() } );

			// Add the custom commands
			if( customCommands != null && customCommands.Count > 0 )
			{
				AddTelnetCommands( (ITelnetCommand[]) customCommands.ToArray( typeof( ITelnetCommand ) ) );
			}

			m_workingThread = new Thread( new ThreadStart( Execute ) );
			m_workingThread.Name = "Telnet Worker Thread: " + GetHashCode();
			m_workingThread.Start();
		}

		/// <summary>
		/// Get the commands listed for this session
		/// </summary>
		public SortedList Commands
		{
			get
			{
				return m_commandsList;
			}
		}

		/// <summary>
		/// Get the commands listed for this session that is listed by command and short cut
		/// </summary>
		public SortedList AllCommands
		{
			get
			{
				return m_allCommandsList;
			}
		}

		/// <summary>
		/// Stop this session and close the port
		/// </summary>
		public void Stop()
		{
			m_done = true;

			// Remove any listeners the user might have running
			m_commandListen.Stop();

			m_tcpClient.GetStream().Close();
			m_tcpClient.Close();
		}

		/// <summary>
		/// Add a group of telnet command to the command processor
		/// </summary>
		/// <param name="commands">Array of command interfaces</param>
		public void AddTelnetCommands( ITelnetCommand[] commands )
		{
			foreach( ITelnetCommand telnetCommand in commands )
			{
				// Add the master command
				string commandName = telnetCommand.Command;
				if( !m_commandsList.Contains( commandName ) )
				{
					m_commandsList.Add( commandName, telnetCommand );
					m_allCommandsList.Add( commandName, telnetCommand );
				}

				// Add all short cuts
				string[] shortCuts = telnetCommand.CommandShortCuts;
				if( shortCuts != null )
				{
					foreach( string shortcut in shortCuts )
					{
						if( !m_allCommandsList.Contains( shortcut ) )
						{
							m_allCommandsList.Add( shortcut, telnetCommand );
						}
					}
				}
			}
		}

		/// <summary>
		/// This method will process the given TextReader and TextWriter interfaces for this commands
		/// object.  Each command line read from the TextReader will be processed and any output will
		/// be written to the TextWriter.
		/// </summary>
		private void Execute()
		{
			try
			{
				// Start by printing a blank line
				m_outStream.WriteLine();

				while( !m_done )
				{
					// Send a prompt to the output stream.
					m_outStream.Write( ">>" );
					m_outStream.Flush();		// Make sure the prompt is seen.

					// Get and trim the text
					string text = HandleInputCharacters();

					// Did we reach the end of the stream?
					if( text != null && text.Length > 0 )
					{
						m_log.DebugFormat( "Execute() saw command \"{0}\"", text );

						// Check to see if this is a comment line.  If not then
						// process the text.
						if( text[ 0 ] != '#' )
						{
							ProcessText( text );
						}

						m_log.DebugFormat( "Execute() finished processing command \"{0}\"", text );
					}
				}
			}
			catch( Exception e )
			{
				m_log.Error("Execute() caught a general exception", e );
			}
		}

		/// <summary>
		/// Called to handle the input characters read out of the input
		/// stream and correctly handle backspaces, up arrows, down arrows, etc.
		/// </summary>
		/// <returns>The text input that needs to be executed.</returns>
		private String HandleInputCharacters()
		{
			// Read the next line of text one character at a time so we can support
			// backspace, a command history, and command tab completion.
			int c = -1;

			// Our "buffer" to keep track of characters we want to keep.
			StringBuilder textBuilder = new StringBuilder();

			// A reverse index into m_historyCommands.  It is incremented or decremented
			// when the user hits the up arrow and down arrow (respectively). 
			int historyCommandIndex = 0;

			do
			{
				c = m_inStream.Read();

				// Is this the end of input?
				if( c == -1 )
				{
					// Yes, return null to indicate the end.  I don't care if there is
					// anything in our buffer.  There is no reason to execute
					// the command if the remote connection has disconnected from us --
					// so don't bother.
					return null;
				}

				// Is it BackSpace key?
				if( c == '\b' && textBuilder.Length > 0 )
				{
					// User has hit delete key but a char is still on screen
					// so we need to "erase" that one char from view
					ClearInputLine( 1 );

					// Remove the char that was deleted from our buffer
					textBuilder.Remove( textBuilder.Length - 1, 1 );

					// Reset our index.
					historyCommandIndex = 0;
				}

					// Is it the Tab character?  If so, use the characters that have
					// been entered up to now and perform a "command completion"
				else if( c == '\t' && textBuilder.Length > 0 )
				{
					// Did the method find some sort of match?
					string theCommand = HandleCommandCompletion( textBuilder.ToString() );
					if( theCommand != null )
					{
						// Yes, Move this command to the output stream.
						MoveCommandToOutStream( theCommand );
								
						// Replace the characters in our buffer with the characters from TheCommand.
						textBuilder = new StringBuilder().Append( theCommand );
					}
					else
					{
						// No, rewrite the characters in our buffer to the output stream.
						MoveCommandToOutStream( textBuilder.ToString() );
					}

					// Reset our index.
					historyCommandIndex = 0;
				}
				// Did we get an escape sequence?  (Look for the up arrow and down arrow.)
				else if( c == '\x1b' )
				{
					c = m_inStream.Read();
					if( c == '[' )
					{
						c = m_inStream.Read();
						// Is it the up arrow?
						if( c == 'A' )
						{
							if( historyCommandIndex < m_historyCommands.Count )
							{
								historyCommandIndex++;
							}

							ShowEnteredCommand( historyCommandIndex, ref textBuilder );
						}
						// Is it the down arrow?
						else if( c == 'B' )
						{
							if( historyCommandIndex > 0 )
							{
								historyCommandIndex--;
							}

							ShowEnteredCommand( historyCommandIndex, ref textBuilder );
						}
					}
				}
				// Any other character that is not the "carriage return" should be added
				// to character buffer.
				else if( c != '\r' )
				{
					// Add it to our buffer.
					textBuilder.Append( (char) c );

					// Reset our index.
					historyCommandIndex = 0;
				}

			}
			while( c != '\n' );

			String text = textBuilder.ToString().Trim();

			// If we got a command always add it to the end of the array list of entered commands.
			AddToHistory( text );

			// Return the command for processing.
			return text;
		}

		/// <summary>
		/// This method will split out the line of text into its indivial tokens
		/// using spaces as the seperator.
		/// </summary>
		/// <param name="text">The command the user entered</param>
		private void ProcessText( string text )
		{
			// Tokenize the string on the spaces.  Use a utility class that works significantly
			// better then the Split method that is implemented on the String class.
			string[] tokens =  text.Split( ' ' );

			// Now build an array from the tokens
			int count = tokens.Length;
			if( count > 0 )
			{
				// Note:  Could be allocating an array of size zero -- which is legal.
				string[] args = new String[ count - 1 ];
				string command = tokens[ 0 ].Trim();

				if( count > 1 )
				{
					for( int x = 1; x < count; x++ )
					{
						args[ x - 1 ] = tokens[ x ].Trim();
					}
				}
				
				ITelnetCommand telnetCommand = (ITelnetCommand) m_allCommandsList[ command ];
				if( telnetCommand != null )
				{
					try
					{
						telnetCommand.ExecCommand( m_outStream, args );
					}
					catch( Exception e )
					{
						m_outStream.WriteLine();
						m_outStream.WriteLine( "Exception processing command {0}: {1}", command, e.Message );
						m_outStream.WriteLine( e.StackTrace );
						m_outStream.WriteLine();
					}
				}
				else
				{
					m_outStream.WriteLine();
					m_outStream.WriteLine( "Unknown command " + command );
					m_outStream.WriteLine();
				}
			}
		}

		/// <summary>
		/// Clears the input line up to a specified number of characters.
		/// </summary>
		/// <param name="numberOfChars">Number of characters to "clear"</param>
		private void ClearInputLine( int numberOfChars )
		{
			for( int x = 0; x < numberOfChars; x++ )
			{
				m_outStream.Write( ' ' );
			}

			for( int x = 0; x < numberOfChars; x++ )
			{
				m_outStream.Write( '\b' );
			}

			m_outStream.Flush();
		}

		/// <summary>
		/// Called to move the text command passed in to the Output Stream.
		/// </summary>
		/// <param name="theCommand">The command that will be written to the output stream</param>
		private void MoveCommandToOutStream( string theCommand )
		{
			// Note that we could write the new command on a new line,
			// but it is more unix-like to replace the exisiting
			// input line (rewrite it) with the new command and clear out
			// any old characters that may be present
			m_outStream.Write( "\r>>" );
			m_outStream.Flush();

			// Clear out the input line an arbitrary number of characters.
			ClearInputLine( 80 );
						
			m_outStream.Write( theCommand );
			m_outStream.Flush();
		}

		/// <summary>
		/// Show a previously entered command.
		/// </summary>
		/// <param name="reverseIndex">A reverse index into the history list.</param>
		/// <param name="textBuilder">StringBuilder which is acting as our character buffer.</param>
		private void ShowEnteredCommand( int reverseIndex, ref StringBuilder textBuilder )
		{
			if( reverseIndex > 0 && reverseIndex <= m_historyCommands.Count && m_historyCommands.Count > 0 )
			{
				string text = (string) m_historyCommands[ m_historyCommands.Count - reverseIndex ];

				MoveCommandToOutStream( text );

				textBuilder = new StringBuilder().Append( text );
			}
		}

		/// <summary>
		/// Called when the TAB character is seen to perform the command
		/// completion using the Custom Command objects stored in this
		/// Commands.
		/// </summary>
		/// <param name="commandFragment">
		/// The command fragment that should contain at least one character for us to match on.
		/// </param>
		/// <returns>
		/// Reference to the first command we find that matches the characters
		/// passed in.  If no commands match then null is returned to the
		/// caller to indicate there is nothing to do.
		/// </returns>
		private string HandleCommandCompletion( string commandFragment )
		{
			commandFragment = commandFragment.ToLower();

			foreach( string command in m_allCommandsList.Keys )
			{
				if( command.StartsWith( commandFragment ) )
				{
					return command;
				}
				else if( commandFragment.CompareTo( command ) < 0 )
				{
					// This is a sorted list (by key) so if the fragment is
					// now less then the current item, we're done.
					break;
				}
			}

			return null;
		}

		/// <summary>
		/// Simple routine that is called to add a command to the end of the Entered Commands 
		/// list.  If the text value passed in is null or the empty string then nothing is 
		/// added.  If the last command in the list is the same as the current command being 
		/// performed, nothing is added.  If the command passed in is different from the last 
		/// command it is added to the list.
		/// </summary>
		/// <param name="command">The command to add to the history</param>
		private void AddToHistory( string command )
		{
			// Don't add blank commands
			if( command != null && command.Length > 0 )
			{
				// If command is not in list then add it.
				if( !m_historyCommands.Contains( command ) )
				{
					m_historyCommands.Add( command );
				}
			}
		}
	}
}
